"""Main game loop and orchestration."""

from __future__ import annotations

import sys
from datetime import datetime
from pathlib import Path
from typing import List

import pygame

from config import BACKGROUND_COLORS, FPS, WORLD_CONFIG
from crafting import Recipe, build_default_recipes
from entities import Enemy, Player
from game_state import GameState
from save_system import SAVE_VERSION, load_payload, save_payload
from tasks import TaskManager
from ui import draw_backpack_overlay, draw_controls, draw_crafting_overlay, draw_hud, draw_task_panel, get_font
from world import Block, Item, World


ITEM_DISPLAY_NAMES = {
    "wood": "木头",
    "plank": "木板",
    "stone": "石头",
    "stone_brick": "石砖",
    "ore_gem": "宝石",
}


def _inventory_sort_key(item_name: str) -> int:
    order = tuple(ITEM_DISPLAY_NAMES.keys())
    try:
        return order.index(item_name)
    except ValueError:
        return len(order)


class Game:
    def __init__(self) -> None:
        pygame.init()
        info = pygame.display.Info()
        self.screen_width = info.current_w
        self.screen_height = info.current_h
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height), pygame.FULLSCREEN)
        pygame.display.set_caption("我的世界（儿童版）")

        self.clock = pygame.time.Clock()
        self.running = True

        self.state = GameState()
        self.world = World(self.screen_width, self.screen_height)
        self.player = Player(100, self.world.ground_height - WORLD_CONFIG.block_size * 3)
        self.enemies: List[Enemy] = []
        self.enemy_spawn_cooldown = 5.0

        self.task_manager = TaskManager(self.state)
        self.prev_day_phase = self.state.day_phase
        self.save_path = Path("saves") / "slot1.json"
        self.message_text = ""
        self.message_timer = 0.0

        self.backpack_open = False
        self.crafting_open = False
        self.backpack_selection = 0
        self.crafting_selection = 0
        self.recipes: List[Recipe] = build_default_recipes()

        self.controls_text = (
            "操作说明:",
            "方向键: 移动角色",
            "空格键: 消耗体力跳跃",
            "鼠标左键: 放置木板(若有)或草地",
            "鼠标右键: 破坏方块并掉落物品",
            "B 键: 打开/关闭背包",
            "C 键: 打开/关闭合成台",
            "回车键: 菜单中确认",
            "Q 键: 背包中丢弃选中物品",
            "R 键: 重新生成地图",
            "E 键: 开关敌人系统",
            "F5 键: 保存进度",
            "F9 键: 读取进度",
            "ESC: 退出游戏",
        )

        self.selected_block = "plank"
        self._set_message("提示: 发光边框的方块可获取石头或宝石", duration=5.0)

    # --- main loop --------------------------------------------------
    def run(self) -> None:
        while self.running:
            delta = self.clock.tick(FPS) / 1000.0
            self._handle_events()
            self._update(delta)
            self._render()
        pygame.quit()
        sys.exit()

    # --- events -----------------------------------------------------
    def _handle_events(self) -> None:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN:
                self._handle_keydown(event)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                self._handle_mouse(event)

    def _handle_keydown(self, event: pygame.event.Event) -> None:
        if event.key == pygame.K_ESCAPE:
            if self.crafting_open:
                self._toggle_crafting()
            elif self.backpack_open:
                self._toggle_backpack()
            else:
                self.running = False
            return

        if self.backpack_open:
            self._handle_backpack_key(event)
            return
        if self.crafting_open:
            self._handle_crafting_key(event)
            return

        if event.key == pygame.K_SPACE:
            self.player.try_jump(self.state)
        elif event.key == pygame.K_r:
            self.world.regenerate()
            self._set_message("提示: 发光边框的方块可获取石头或宝石", duration=5.0)
        elif event.key == pygame.K_b:
            self._toggle_backpack()
        elif event.key == pygame.K_c:
            self._toggle_crafting()
        elif event.key == pygame.K_e:
            self.state.toggle_enemy_system()
            if not self.state.enemy_system_active:
                self.enemies.clear()
        elif event.key == pygame.K_F5:
            self._save_game()
        elif event.key == pygame.K_F9:
            self._load_game()
        elif event.key == pygame.K_1:
            self.selected_block = "plank"
        elif event.key == pygame.K_2:
            self.selected_block = "grass"
        elif event.key == pygame.K_3:
            self.selected_block = "stone_brick"

    def _handle_mouse(self, event: pygame.event.Event) -> None:
        mouse_x, mouse_y = pygame.mouse.get_pos()
        x = (mouse_x // WORLD_CONFIG.block_size) * WORLD_CONFIG.block_size
        y = (mouse_y // WORLD_CONFIG.block_size) * WORLD_CONFIG.block_size

        if event.button == 1:
            self._place_block(x, y)
        elif event.button == 3:
            self._break_block(mouse_x, mouse_y)

    # --- gameplay updates ------------------------------------------
    def _update(self, delta: float) -> None:
        if self.backpack_open or self.crafting_open:
            if self.message_timer > 0.0:
                self.message_timer = max(0.0, self.message_timer - delta)
            return

        self.state.update_time(delta)
        self._check_day_transition()

        keys = pygame.key.get_pressed()
        self.player.handle_input(keys, delta, self.world)
        self.player.update(delta, self.world, self.state)
        self._update_items()
        self._update_enemies(delta)
        self.task_manager.update()
        if self.message_timer > 0.0:
            self.message_timer = max(0.0, self.message_timer - delta)

    def _check_day_transition(self) -> None:
        if self.prev_day_phase == "night" and self.state.day_phase == "day" and self.state.enemy_system_active:
            self.task_manager.record_event("nights_survived", 1)
        self.prev_day_phase = self.state.day_phase

    def _place_block(self, x: int, y: int) -> None:
        if self.world.block_at(x, y):
            return
        block_type = self.selected_block
        if block_type == "plank":
            if self.state.remove_item("plank", 1):
                if self.world.place_block(x, y, "plank"):
                    self.task_manager.record_event("base_blocks", 1)
            else:
                # 无木板时改为放置草地
                self.world.place_block(x, y, "grass")
        else:
            self.world.place_block(x, y, block_type)

    def _break_block(self, mouse_x: int, mouse_y: int) -> None:
        target = self._locate_block_for_break(mouse_x, mouse_y)
        if not target:
            return
        x, y = target
        drop = self.world.remove_block(x, y)
        if drop:
            self.world.spawn_item(x, y, drop)

    def _locate_block_for_break(self, mouse_x: int, mouse_y: int) -> tuple[int, int] | None:
        block_size = WORLD_CONFIG.block_size
        aligned_x = (mouse_x // block_size) * block_size
        aligned_y = (mouse_y // block_size) * block_size

        block = self.world.block_at(aligned_x, aligned_y)
        if block:
            return block.x, block.y

        resource_target = self._resource_block_from_mouse(mouse_x, mouse_y)
        if resource_target:
            return resource_target

        return None

    def _resource_block_from_mouse(self, mouse_x: int, mouse_y: int) -> tuple[int, int] | None:
        block_size = WORLD_CONFIG.block_size
        for (hint_x, hint_y), _ in self.world.resource_hints.items():
            hint_rect = pygame.Rect(hint_x - 6, hint_y - 18, block_size + 12, block_size + 24)
            if hint_rect.collidepoint(mouse_x, mouse_y):
                block = self.world.block_at(hint_x, hint_y)
                if block:
                    return block.x, block.y

        aligned_x = (mouse_x // block_size) * block_size
        below_y = ((mouse_y // block_size) + 1) * block_size
        block_below = self.world.block_at(aligned_x, below_y)
        if block_below and block_below.block_type in {"stone", "ore"}:
            return block_below.x, block_below.y

        return None

    def _update_items(self) -> None:
        pickup_rect = self.player.rect.inflate(40, 40)
        for item in list(self.world.items):
            item_rect = pygame.Rect(item.x, item.y, WORLD_CONFIG.block_size, WORLD_CONFIG.block_size)
            if pickup_rect.colliderect(item_rect):
                if self.state.add_item(item.item_type):
                    self.world.remove_item(item)
                else:
                    self._set_message("背包已满，无法拾取")

    def _update_enemies(self, delta: float) -> None:
        if self.state.enemy_system_active and self.state.day_phase == "night":
            self.enemy_spawn_cooldown -= delta
            if self.enemy_spawn_cooldown <= 0:
                self._spawn_enemy_wave()
                self.enemy_spawn_cooldown = 6.0
        else:
            self.enemy_spawn_cooldown = 5.0

        for enemy in list(self.enemies):
            enemy.update(self.world, self.state, delta)
            if not self.state.enemy_system_active and self.state.day_phase == "day":
                self.enemies.remove(enemy)
                continue
            if self.player.rect.colliderect(enemy.rect):
                self.player.take_damage(1, self.state)
                if not self.state.is_alive:
                    self.running = False

    def _spawn_enemy_wave(self) -> None:
        spawn_positions = [200, self.screen_width // 2, self.screen_width - 200]
        for pos in spawn_positions:
            enemy = Enemy.create(pos, self.world.ground_height - WORLD_CONFIG.block_size * 2)
            self.enemies.append(enemy)

    # --- rendering --------------------------------------------------
    def _render(self) -> None:
        background = BACKGROUND_COLORS.get(self.state.day_phase, BACKGROUND_COLORS["day"])
        self.screen.fill(background)

        self.world.draw_blocks(self.screen)
        self.world.draw_items(self.screen)
        self.world.draw_resource_hints(self.screen)

        for enemy in self.enemies:
            enemy.draw(self.screen)

        self.player.draw(self.screen)

        draw_controls(self.screen, self.controls_text)
        controls_bottom = 20 + len(self.controls_text) * 32
        task_panel_y = controls_bottom + 20
        draw_task_panel(self.screen, self.task_manager, task_panel_y)
        draw_hud(self.screen, self.state, self.screen_width, self.screen_height)

        status_font = get_font(24)
        enemy_status = "开启" if self.state.enemy_system_active else "关闭"
        status_surface = status_font.render(f"敌人系统: {enemy_status}", True, (255, 255, 255))
        self.screen.blit(status_surface, (20, self.screen_height - 100))

        if self.message_timer > 0.0 and self.message_text:
            notice_font = get_font(28)
            notice_surface = notice_font.render(self.message_text, True, (255, 255, 0))
            self.screen.blit(notice_surface, (20, self.screen_height - 140))

        if self.backpack_open:
            draw_backpack_overlay(
                self.screen,
                self._inventory_items(),
                self.backpack_selection,
                self.state.inventory_capacity,
                self.state.total_items(),
                self.selected_block,
                ITEM_DISPLAY_NAMES,
            )
        if self.crafting_open:
            draw_crafting_overlay(
                self.screen,
                self.recipes,
                self.crafting_selection,
                self.state.inventory,
                ITEM_DISPLAY_NAMES,
            )

        pygame.display.flip()

    # --- persistence ---------------------------------------------
    def _save_game(self) -> None:
        payload = {
            "version": SAVE_VERSION,
            "timestamp": datetime.now().isoformat(),
            "state": self._serialize_state(),
        }
        try:
            save_payload(self.save_path, payload)
            self._set_message("进度已保存")
        except Exception as exc:  # pragma: no cover - log path
            print(f"保存存档失败: {exc}", file=sys.stderr)
            self._set_message("保存失败")

    def _load_game(self) -> None:
        if not self.save_path.exists():
            self._set_message("未找到存档")
            return
        try:
            payload = load_payload(self.save_path)
        except Exception as exc:  # pragma: no cover - log path
            print(f"读取存档失败: {exc}", file=sys.stderr)
            self._set_message("读取失败")
            return
        if payload.get("version") != SAVE_VERSION:
            self._set_message("存档版本不兼容")
            return
        try:
            self._apply_serialized_state(payload.get("state", {}))
            self._set_message("读取成功")
        except Exception as exc:  # pragma: no cover - log path
            print(f"应用存档失败: {exc}", file=sys.stderr)
            self._set_message("存档损坏")

    def _serialize_state(self) -> dict[str, object]:
        rect = self.player.rect
        game_state = {
            "inventory": dict(self.state.inventory),
            "health": self.state.health,
            "stamina": self.state.stamina,
            "enemy_system_active": self.state.enemy_system_active,
            "time_elapsed": self.state.time_elapsed,
            "day_phase": self.state.day_phase,
            "current_task": self.state.current_task,
            "task_progress": dict(self.state.task_progress),
        }
        player_state = {
            "rect": [rect.x, rect.y, rect.width, rect.height],
            "velocity_y": self.player.velocity_y,
            "on_ground": self.player.on_ground,
            "invulnerability_timer": self.player.invulnerability_timer,
        }
        world_state = {
            "blocks": [
                {"x": block.x, "y": block.y, "block_type": block.block_type}
                for block in self.world.blocks
            ],
            "items": [
                {"x": item.x, "y": item.y, "item_type": item.item_type}
                for item in self.world.items
            ],
        }
        enemies_state = [
            {
                "rect": [enemy.rect.x, enemy.rect.y, enemy.rect.width, enemy.rect.height],
                "direction": enemy.direction,
                "base_y": enemy.base_y,
            }
            for enemy in self.enemies
        ]
        return {
            "game_state": game_state,
            "player": player_state,
            "world": world_state,
            "enemies": enemies_state,
            "enemy_spawn_cooldown": self.enemy_spawn_cooldown,
            "selected_block": self.selected_block,
            "prev_day_phase": self.prev_day_phase,
            "task_manager": {"active_task_id": self.task_manager.active_task_id},
        }

    def _apply_serialized_state(self, data: dict[str, object]) -> None:
        game_state = data.get("game_state", {}) if isinstance(data, dict) else {}
        player_state = data.get("player", {}) if isinstance(data, dict) else {}
        world_state = data.get("world", {}) if isinstance(data, dict) else {}

        if isinstance(game_state, dict):
            inventory_source = game_state.get("inventory", {})
            if isinstance(inventory_source, dict):
                self.state.inventory = {str(k): int(v) for k, v in inventory_source.items()}
            self.state.health = int(game_state.get("health", self.state.health))
            self.state.stamina = int(game_state.get("stamina", self.state.stamina))
            self.state.enemy_system_active = bool(game_state.get("enemy_system_active", False))
            self.state.time_elapsed = float(game_state.get("time_elapsed", self.state.time_elapsed))
            if isinstance(game_state.get("day_phase"), str):
                self.state.day_phase = game_state["day_phase"]
            self.state.current_task = game_state.get("current_task")
            progress_source = game_state.get("task_progress", {})
            if isinstance(progress_source, dict):
                self.state.task_progress = {str(k): int(v) for k, v in progress_source.items()}

        if isinstance(player_state, dict):
            rect_values = player_state.get("rect", [self.player.rect.x, self.player.rect.y, self.player.rect.width, self.player.rect.height])
            if isinstance(rect_values, list) and len(rect_values) == 4:
                self.player.rect.update(*map(int, rect_values))
            self.player.velocity_y = float(player_state.get("velocity_y", self.player.velocity_y))
            self.player.on_ground = bool(player_state.get("on_ground", self.player.on_ground))
            self.player.invulnerability_timer = float(player_state.get("invulnerability_timer", 0.0))

        if isinstance(world_state, dict):
            blocks = []
            for block in world_state.get("blocks", []):
                if isinstance(block, dict):
                    blocks.append(Block(int(block.get("x", 0)), int(block.get("y", 0)), str(block.get("block_type", "grass"))))
            items = []
            for item in world_state.get("items", []):
                if isinstance(item, dict):
                    items.append(Item(int(item.get("x", 0)), int(item.get("y", 0)), str(item.get("item_type", "wood"))))
            self.world.blocks = blocks
            self.world.items = items

        enemies_data = data.get("enemies", []) if isinstance(data, dict) else []
        self.enemies.clear()
        for enemy in enemies_data:
            if isinstance(enemy, dict):
                rect_data = enemy.get("rect", [0, 0, 0, 0])
                if isinstance(rect_data, list) and len(rect_data) == 4:
                    rect = pygame.Rect(*map(int, rect_data))
                    direction = int(enemy.get("direction", 1)) or 1
                    base_y = int(enemy.get("base_y", rect.y))
                    self.enemies.append(Enemy(rect=rect, direction=direction, base_y=base_y))

        self.enemy_spawn_cooldown = float(data.get("enemy_spawn_cooldown", 5.0)) if isinstance(data, dict) else 5.0
        if isinstance(data, dict) and isinstance(data.get("selected_block"), str):
            self.selected_block = data["selected_block"]
        if isinstance(data, dict) and isinstance(data.get("prev_day_phase"), str):
            self.prev_day_phase = data["prev_day_phase"]

        task_info = data.get("task_manager", {}) if isinstance(data, dict) else {}
        saved_task_id = task_info.get("active_task_id") if isinstance(task_info, dict) else None
        if saved_task_id in self.task_manager.tasks:
            self.task_manager.active_task_id = saved_task_id
        else:
            self.task_manager.active_task_id = next(iter(self.task_manager.tasks), None)
        self.state.current_task = self.task_manager.active_task_id

    def _set_message(self, text: str, duration: float = 3.0) -> None:
        self.message_text = text
        self.message_timer = duration

    # --- backpack helpers ---------------------------------------
    def _toggle_backpack(self) -> None:
        if self.crafting_open:
            self.crafting_open = False
        self.backpack_open = not self.backpack_open
        if self.backpack_open:
            self.backpack_selection = 0

    def _toggle_crafting(self) -> None:
        if not self.recipes:
            self._set_message("暂无可用配方")
            return
        if self.backpack_open:
            self.backpack_open = False
        self.crafting_open = not self.crafting_open
        if self.crafting_open:
            self.crafting_selection = 0

    def _inventory_items(self) -> list[tuple[str, int]]:
        return [
            (name, count)
            for name, count in sorted(self.state.inventory.items(), key=lambda item: _inventory_sort_key(item[0]))
            if count > 0
        ]

    def _handle_backpack_key(self, event: pygame.event.Event) -> None:
        items = self._inventory_items()
        if event.key in (pygame.K_LEFT, pygame.K_UP):
            if items:
                self.backpack_selection = (self.backpack_selection - 1) % len(items)
        elif event.key in (pygame.K_RIGHT, pygame.K_DOWN):
            if items:
                self.backpack_selection = (self.backpack_selection + 1) % len(items)
        elif event.key == pygame.K_RETURN:
            self._activate_backpack_selection()
        elif event.key == pygame.K_q:
            self._drop_selected_item()
        elif event.key == pygame.K_c:
            self._toggle_crafting()
        elif event.key == pygame.K_b:
            self._toggle_backpack()

    def _handle_crafting_key(self, event: pygame.event.Event) -> None:
        if not self.recipes:
            self._toggle_crafting()
            return
        if event.key in (pygame.K_LEFT, pygame.K_UP):
            self.crafting_selection = (self.crafting_selection - 1) % len(self.recipes)
        elif event.key in (pygame.K_RIGHT, pygame.K_DOWN):
            self.crafting_selection = (self.crafting_selection + 1) % len(self.recipes)
        elif event.key == pygame.K_RETURN:
            self._craft_selected_recipe()
        elif event.key == pygame.K_b:
            self._toggle_backpack()
        elif event.key == pygame.K_c:
            self._toggle_crafting()

    def _activate_backpack_selection(self) -> None:
        items = self._inventory_items()
        if not items:
            self._set_message("背包为空")
            return
        if self.backpack_selection >= len(items):
            self.backpack_selection = len(items) - 1
        name, _ = items[self.backpack_selection]
        if self._is_placeable_item(name):
            self.selected_block = name
            self._set_message(f"已选择 {ITEM_DISPLAY_NAMES.get(name, name)}")
        else:
            self._set_message("这件物品无法直接使用")

    def _drop_selected_item(self) -> None:
        items = self._inventory_items()
        if not items:
            self._set_message("背包为空")
            return
        if self.backpack_selection >= len(items):
            self.backpack_selection = len(items) - 1
        name, _ = items[self.backpack_selection]
        if not self.state.remove_item(name, 1):
            self._set_message("没有足够的物品可以丢弃")
            return
        drop_x = self.player.rect.centerx - WORLD_CONFIG.block_size // 2
        drop_y = self.player.rect.bottom
        self.world.spawn_item(drop_x, drop_y, name)
        self._set_message(f"已丢弃 {ITEM_DISPLAY_NAMES.get(name, name)}")
        remaining = self._inventory_items()
        if not remaining:
            self.backpack_selection = 0
        else:
            self.backpack_selection = min(self.backpack_selection, len(remaining) - 1)

    def _craft_selected_recipe(self) -> None:
        if not self.recipes:
            return
        recipe = self.recipes[self.crafting_selection]
        if not recipe.can_craft(self.state.inventory):
            self._set_message("材料不足")
            return
        if self.state.inventory_capacity is not None:
            total_before = self.state.total_items()
            net_change = sum(recipe.outputs.values()) - sum(recipe.inputs.values())
            if net_change > 0 and total_before + net_change > self.state.inventory_capacity:
                self._set_message("背包空间不足")
                return
        if not self.state.consume_items(recipe.inputs):
            self._set_message("材料不足")
            return
        added_successfully = True
        for name, amount in recipe.outputs.items():
            if not self.state.add_item(name, amount):
                added_successfully = False
                break
        if not added_successfully:
            # rollback outputs and restore inputs
            for name, amount in recipe.outputs.items():
                self.state.remove_item(name, amount)
            for name, amount in recipe.inputs.items():
                self.state.add_item(name, amount)
            self._set_message("背包空间不足")
            return
        self._set_message(f"合成成功: {recipe.name}")
        items = self._inventory_items()
        if items:
            self.backpack_selection = min(self.backpack_selection, len(items) - 1)

    def _is_placeable_item(self, item_type: str) -> bool:
        return item_type in {"plank", "stone_brick", "grass"}
